// Copyright (c) 2014-present, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package file includes file related manipulations.
package file

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strconv"
	"strings"

	"chainmaker.org/chainmaker/smarteditor/common"

	"chainmaker.org/chainmaker/smarteditor/conf"
	"chainmaker.org/chainmaker/smarteditor/logger"
	"chainmaker.org/chainmaker/smarteditor/session"
	"chainmaker.org/chainmaker/smarteditor/util"

	"github.com/88250/gulu"
)

const (
	//IcoZTreeDir ztree dir ico
	IcoZTreeDir = "ico-ztree-dir "
	//IcoWorkspace workspace icon
	IcoWorkspace = "ico-ztree-dir-workspace "
)

var log = logger.Get()

// Node represents a file node in file tree.
type Node struct {
	ID        string  `json:"id"`
	Name      string  `json:"name"`
	Path      string  `json:"path"`
	IconSkin  string  `json:"iconSkin"` // Value should be end with a space
	IsParent  bool    `json:"isParent"`
	Type      string  `json:"type"`      // "f": file, "d": directory
	Creatable bool    `json:"creatable"` // whether can create file in this file node
	Removable bool    `json:"removable"` // whether can remove this file node
	IsGoAPI   bool    `json:"isGOAPI"`
	Mode      string  `json:"mode"`
	Children  []*Node `json:"children"`
}

// Snippet represents a source code snippet, used to as the result of "Find Usages", "Search".
type Snippet struct {
	Path     string   `json:"path"`     // file path
	Line     int      `json:"line"`     // line number
	Ch       int      `json:"ch"`       // column number
	Contents []string `json:"contents"` // lines nearby
}

//NameAndPath file name and file path
type NameAndPath struct {
	Name string `json:"name"`
	Path string `json:"path"`
}

var apiNode *Node

// initAPINode builds the Go API file node.
func initAPINode() {
	apiPath := gulu.Go.GetAPIPath()

	apiNodeSdk := &Node{Name: fmt.Sprintf("Go SDK(%s)", runtime.Version()), Path: apiPath, IconSkin: "ico-ztree-dir-api ", Type: "d",
		Creatable: false, Removable: false, IsGoAPI: true, Children: []*Node{}}
	walk(apiPath, apiNodeSdk, false, false, true, "", nil)

	apiNode = &Node{Name: "External Libraries", Path: "", IconSkin: "ico-ztree-dir-api ", Type: "d",
		Creatable: false, Removable: false, IsGoAPI: true, Children: []*Node{apiNodeSdk}}
}

// GetFilesHandler handles request of constructing user workspace file tree.
//
// The Go API source code package also as a child node,
// so that users can easily view the Go API source code in file tree.
func GetFilesHandler(w http.ResponseWriter, r *http.Request) {
	uid, err := session.GetUidFromSession(r)
	if err != nil {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetGzResult(w, r, result)

	var args map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Errorf("GetFilesHandler decode parameter failed. url:%s, err:%v", r.URL, err)
		result.Code = -1
		return
	}
	user := conf.GetUser(uid)
	hidden, hiddenErr := conf.GetUserHiddenContract(uid)
	if hiddenErr != nil {
		log.Errorf("RefreshDirectoryHandler GetUserHiddenContract uid %s, failed %s", uid, hiddenErr.Error())
		http.Error(w, hiddenErr.Error(), http.StatusInternalServerError)
		return
	}
	hiddenMp := make(map[string]struct{})
	for _, temp := range hidden {
		hiddenMp[temp.Contract] = struct{}{}
	}
	contractName := args["contractName"].(string)

	userWorkspace := conf.GetUserWorkspace(uid)
	workspaces := filepath.SplitList(userWorkspace)

	root := Node{Name: "root", Path: "", IconSkin: IcoZTreeDir, Type: "d", IsParent: true, Children: []*Node{}}

	if nil == apiNode { // lazy init
		initAPINode()
	}

	index := 0
	workspaceName := ""

	// workspace node process
	for _, workspace := range workspaces {
		workspaceName = "Workspace"
		if index != 0 {
			workspaceName = workspaceName + "_" + strconv.Itoa(index)
		}
		index++

		workspacePath := workspace + conf.PathSeparator + "src"
		workspaceNode := Node{
			ID:        filepath.ToSlash(workspacePath), // jQuery API can't accept "\", so we convert it to "/"
			Name:      workspaceName,
			Path:      filepath.ToSlash(workspacePath),
			IconSkin:  IcoWorkspace,
			Type:      "d",
			Creatable: true,
			Removable: false,
			IsGoAPI:   false,
			Children:  []*Node{},
		}
		walk(workspacePath, &workspaceNode, true, true, false, user.Workspace, hiddenMp)
		if contractName != "" {
			filterChild := make([]*Node, 0)
			for i := range workspaceNode.Children {
				if workspaceNode.Children[i].Name == contractName {
					filterChild = append(filterChild, workspaceNode.Children[i])
				}
			}
			workspaceNode.Children = filterChild
		}
		root.Children = append(root.Children, &workspaceNode)
	}

	root.Children = append(root.Children, apiNode)
	result.Data = root
}

//GetGoModulesHandler get Go Modules for contract
func GetGoModulesHandler(w http.ResponseWriter, r *http.Request) {
	uid, err := session.GetUidFromSession(r)
	if err != nil {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetGzResult(w, r, result)
	user := conf.GetUser(uid)
	hidden, hiddenErr := conf.GetUserHiddenContract(uid)
	if hiddenErr != nil {
		log.Errorf("RefreshDirectoryHandler GetUserHiddenContract uid %s, failed %s", uid, hiddenErr.Error())
		http.Error(w, hiddenErr.Error(), http.StatusInternalServerError)
		return
	}
	hiddenMp := make(map[string]struct{})
	for _, temp := range hidden {
		hiddenMp[temp.Contract] = struct{}{}
	}
	var args map[string]interface{}
	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		log.Errorf("GetFilesHandler decode parameter failed. url:%s, err:%v", r.URL, err)
		result.Code = -1
		return
	}

	contractName := args["contractName"].(string)
	workspace := conf.GetUserWorkspace(uid)
	userGoModulePath := filepath.Join(util.RuntimeGoPath, conf.GoModulePath())
	goModuleRootNode := getAPINode("Go Modules", userGoModulePath)

	goSumFile := filepath.Join(workspace, conf.Src(), contractName, util.GoSumFile)
	modulePathList, err := readDependenceFromGoSum(goSumFile)
	if err != nil {
		result.Data = goModuleRootNode
		return
	}

	for _, path := range modulePathList {
		if !util.FileExists(filepath.Join(userGoModulePath, path)) {
			continue
		}
		goModuleNode := getAPINode(strings.ReplaceAll(path, "@", " "), path)
		goModuleRootNode.Children = append(goModuleRootNode.Children, goModuleNode)
		walk(filepath.Join(userGoModulePath, path), goModuleNode, false, false, true, user.Workspace, hiddenMp)
	}

	result.Data = goModuleRootNode
	return
}

func getAPINode(name, path string) *Node {
	return &Node{
		Name:      name,
		Path:      path,
		IconSkin:  "ico-ztree-dir-api ",
		Type:      "d",
		Creatable: false,
		Removable: false,
		IsGoAPI:   true,
		Children:  []*Node{},
	}
}

func readDependenceFromGoSum(goSumFile string) ([]string, error) {
	if !util.FileExists(goSumFile) {
		return nil, nil
	}

	file, err := os.OpenFile(goSumFile, os.O_RDONLY, 0)
	if err != nil {
		return nil, err
	}

	pathList := make([]string, 0)
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		lineText := scanner.Text()
		moduleFilePath, err := GetFilePathFromLine(lineText)
		if err != nil {
			continue
		}
		pathList = append(pathList, moduleFilePath)
	}
	return pathList, nil
}

func GetFilePathFromLine(lineText string) (string, error) {
	column := strings.Split(lineText, " ")
	if len(column) <= 1 {
		return "", errors.New("parse file fail")
	}
	if !strings.HasSuffix(column[1], util.GoModFile) {
		return "", errors.New("parse file fail")
	}

	modFilePath := column[0] + "@" + column[1]
	systemFilePath := strings.ReplaceAll(modFilePath, "/", string(os.PathSeparator))
	return filepath.Dir(systemFilePath), nil
}

// GetCompileDirectory handle request of constructing user workspace file tree.
//
// The Go API source code package also as a child node,
// so that users can easily view the Go API source code in file tree.
func GetCompileDirectory(w http.ResponseWriter, r *http.Request) {
	httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
	if httpSession.IsNew {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}
	uid := httpSession.Values["uid"].(string)

	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetGzResult(w, r, result)

	hidden, hiddenErr := conf.GetUserHiddenContract(uid)
	if hiddenErr != nil {
		log.Errorf("GetCompileDirectory GetUserHiddenContract uid %s,failed %s",
			uid, hiddenErr.Error())
		result.Code = -1
		result.Msg = "internal error"
		return
	}
	hiddenMp := make(map[string]struct{})
	for i := 0; i < len(hidden); i++ {
		hiddenMp[hidden[i].Contract] = struct{}{}
	}
	workSpaces := filepath.SplitList(conf.GetUserWorkspace(uid))
	namePaths := make([]NameAndPath, 0)
	for _, workspace := range workSpaces {
		srcPath := filepath.Join(workspace, "src")
		file, err := os.OpenFile(srcPath, os.O_RDONLY, 0)
		if err != nil {
			result.Code = -1
			result.Msg = "internal error"
			return
		}
		defer file.Close()
		fileNames, err := file.Readdirnames(-1)
		if err != nil {
			result.Code = -1
			result.Msg = "internal error"
			return
		}
		for _, fileName := range fileNames {
			path := filepath.Join(srcPath, fileName)
			if !gulu.File.IsDir(path) {
				continue
			}
			_, ok := hiddenMp[fileName]
			if ok {
				continue
			}
			namePaths = append(namePaths, NameAndPath{
				Name: fileName,
				Path: path + conf.PathSeparator,
			})
		}
	}
	result.Code = 0
	result.Data = namePaths
}

func contractHasBuild(workspace string, contractName string) bool {
	execDir := conf.GetExecDirName(workspace)
	executableFileSuffix := common.GetExecutableFileSuffix(conf.Docker)
	executableFileName := filepath.Join(execDir, contractName) + executableFileSuffix
	return util.FileExists(executableFileName)
}

// RefreshDirectoryHandler handles request of refresh a directory of file tree.
func RefreshDirectoryHandler(w http.ResponseWriter, r *http.Request) {
	httpSession, _ := session.HTTPSession.Get(r, session.CookieName)
	if httpSession.IsNew {
		http.Error(w, "Forbidden", http.StatusForbidden)

		return
	}
	uid := httpSession.Values["uid"].(string)

	path := ""
	contentType := r.Header.Get("Content-Type")
	if contentType == "application/x-www-form-urlencoded" {
		err := r.ParseForm()
		if err != nil {
			log.Errorf("parse parameters failed, err:%v", err)
			http.Error(w, "parse parameters failed", http.StatusForbidden)
			return
		}
		path = r.PostForm.Get("path")
	} else {
		var args map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
			log.Error(err)
			http.Error(w, "parameters error", http.StatusForbidden)
			return
		}
		path = args["path"].(string)
	}

	if !gulu.Go.IsAPI(path) && !util.BelongsGoPath(path) && !session.CanAccess(uid, path) {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}
	user := conf.GetUser(uid)
	hidden, hiddenErr := conf.GetUserHiddenContract(uid)
	if hiddenErr != nil {
		log.Errorf("RefreshDirectoryHandler GetUserHiddenContract uid %s, failed %s", uid, hiddenErr.Error())
		http.Error(w, hiddenErr.Error(), http.StatusInternalServerError)
		return
	}
	hiddenMp := make(map[string]struct{})
	for _, temp := range hidden {
		hiddenMp[temp.Contract] = struct{}{}
	}
	node := Node{Name: "root", Path: path, IconSkin: IcoZTreeDir, Type: "d", Children: []*Node{}}

	walk(path, &node, true, true, false, user.Workspace, hiddenMp)

	w.Header().Set("Content-Type", "application/json")
	data, err := json.Marshal(node.Children)
	if err != nil {
		log.Error(err)
		return
	}
	util.Write(w, data)
}

// GetFileHandler handles request of opening file by editor.
func GetFileHandler(w http.ResponseWriter, r *http.Request) {

	//check session
	uid, err := session.GetUidFromSession(r)
	if err != nil {
		log.Errorf("get file handler. err:%v", err)
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)

	//read args
	args, err := util.ReadArgs(r.Body)
	if err != nil {
		log.Errorf("get file handler. read args fail. err:%v", err)
		result.Code = -1
		return
	}

	path := args["path"].(string)
	if !gulu.Go.IsAPI(path) && !util.BelongsGoPath(path) && !session.CanAccess(uid, path) {
		//http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	size := gulu.File.GetFileSize(path)
	if size > FiveMB { // 5M
		result.Code = -1
		result.Msg = "This file is too large to open :("
		return
	}

	data := map[string]interface{}{}
	result.Data = &data

	buf, _ := ioutil.ReadFile(path)
	extension := filepath.Ext(path)

	if gulu.File.IsImg(extension) {
		// image file will be open in a browser tab

		data["mode"] = "img"

		userID := conf.GetOwner(path)
		if "" == userID {
			log.Warnf("The path [%s] has no owner", path)
			data["path"] = ""

			return
		}

		user := conf.GetUser(uid)

		data["path"] = "/workspace/" + user.Name + "/" + strings.Replace(path, user.WorkspacePath(), "", 1)

		return
	}

	content := string(buf)

	if gulu.File.IsBinary(content) {
		result.Code = -1
		result.Msg = "Can't open a binary file :("
	} else {
		data["content"] = content
		data["path"] = path
	}
}

// walk traverses the specified path to build a file tree.
func walk(path string, node *Node, creatable, removable, isGOAPI bool, userWorkSpace string, skippedContract map[string]struct{}) {
	files := listFiles(path)
	for _, filename := range files {
		filePath := filepath.Join(path, filename)
		if util.IsContractRootDirectory(userWorkSpace, filePath) {
			_, ok := skippedContract[filename]
			if ok {
				continue
			}
		}
		fio, _ := os.Lstat(filePath)

		child := Node{
			ID:        filepath.ToSlash(filePath), // jQuery API can't accept "\", so we convert it to "/"
			Name:      filename,
			Path:      filepath.ToSlash(filePath),
			Removable: removable,
			IsGoAPI:   isGOAPI,
			Children:  []*Node{}}
		node.Children = append(node.Children, &child)

		if nil == fio {
			log.Warnf("Path [%s] is nil", filePath)
			continue
		}
		if fio.IsDir() {
			child.Type = "d"
			child.Creatable = creatable
			child.IconSkin = IcoZTreeDir
			child.IsParent = true
			walk(filePath, &child, creatable, removable, isGOAPI, userWorkSpace, skippedContract)
		} else {
			child.Type = "f"
			child.Creatable = creatable
			ext := filepath.Ext(filePath)
			child.IconSkin = getIconSkin(ext)
		}
	}
	return
}

// listFiles lists names of files under the specified dirname.
func listFiles(dirname string) []string {
	f, _ := os.Open(dirname)
	names, _ := f.Readdirnames(-1)
	util.CloseAndLogError(f)

	sort.Strings(names)
	dirs := make([]string, 0)
	files := make([]string, 0)
	// sort: directories in front of files
	for _, name := range names {
		path := filepath.Join(dirname, name)
		fio, err := os.Lstat(path)
		if nil != err {
			log.Warnf("Can't read file info [%s]", path)
			continue
		}
		if fio.IsDir() {
			if conf.IsExcludeDirectory(fio.Name()) {
				continue
			}
			dirs = append(dirs, name)
		} else {
			ext := filepath.Ext(fio.Name())
			if conf.IsExcludeFile(ext) {
				continue
			}
			files = append(files, name)
		}
	}

	return append(dirs, files...)
}

// getIconSkin gets CSS class name of icon with the specified filename extension.
//
// Refers to the zTree document for CSS class names.
func getIconSkin(filenameExtension string) string {
	if gulu.File.IsImg(filenameExtension) {
		return "ico-ztree-img "
	}

	switch filenameExtension {
	case ".html", ".htm":
		return "ico-ztree-html "
	case ".go":
		return "ico-ztree-go "
	case ".css":
		return "ico-ztree-css "
	case ".txt":
		return "ico-ztree-text "
	case ".sql":
		return "ico-ztree-sql "
	case ".properties":
		return "ico-ztree-pro "
	case ".md":
		return "ico-ztree-md "
	case ".js", ".json":
		return "ico-ztree-js "
	case ".xml":
		return "ico-ztree-xml "
	default:
		return "ico-ztree-other "
	}
}

// Default exclude file name patterns when find.
var defaultExcludesFind = []string{".git", ".svn", ".repository", "CVS", "RCS", "SCCS", ".bzr", ".metadata", ".hg"}

// find finds files under the specified dir and its sub-directories with the specified name,
// likes the command 'find dir -name name'.
func find(dir, name string, results []*string) []*string {
	if !strings.HasSuffix(dir, conf.PathSeparator) {
		dir += conf.PathSeparator
	}

	f, _ := os.Open(dir)
	fileInfos, err := f.Readdir(-1)
	defer util.CloseAndLogError(f)

	if nil != err {
		log.Errorf("Read dir [%s] failed: [%s]", dir, err.Error())
		return results
	}

	for _, fileInfo := range fileInfos {
		fileName := fileInfo.Name()
		filePath := dir + fileName
		if fileInfo.IsDir() {
			if gulu.Str.Contains(fileName, defaultExcludesFind) {
				continue
			}
			// enter the directory recursively
			results = find(filePath, name, results)
		} else {
			// match filename
			pattern := filepath.Dir(filePath) + conf.PathSeparator + name
			match, err := filepath.Match(strings.ToLower(pattern), strings.ToLower(filePath))
			if nil != err {
				log.Errorf("Find match filename failed: [%s]", err.Error())
				continue
			}
			if match {
				results = append(results, &filePath)
			}
		}
	}
	return results
}
